﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using ProjNet.Converters.WellKnownText;
using ProjNet.CoordinateSystems;

namespace HashFoo.Nhib.CoreTests
{
    public static class ProjUtil
    {
        private static Mutex _sridWktLoadLocker = new Mutex(false);
        private static string[] _sridWkt = null;

        private static string[] SridWkt
        {
            get
            {
                _sridWktLoadLocker.WaitOne();

                if (_sridWkt == null) _sridWkt = Srid.Resources.SridWkt.Split(new string[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries);

                _sridWktLoadLocker.ReleaseMutex();
                return _sridWkt;
            }
        }

        /// <summary>
        /// Reads the static internal WktSrid file to get all know transformations.  This is an expensive operation.
        /// </summary>
        /// <remarks>
        /// This is a very expensive operation (almost 1 sec per call).  If you want a single coordinate system, its better to look up off the srid.
        /// <see cref="GetCoordinateSystemFromSrid"/>
        /// </remarks>
        /// <returns></returns>
        public static IEnumerable<ICoordinateSystem> ReadAllFromWkt()
        {
            var projectionLines = Srid.Resources.SridWkt.Split(new string[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries);

            var projections = new List<ICoordinateSystem>();
            foreach (var proj in projectionLines)
            {
                var lineToks = proj.Split(';');
                if (lineToks.Length != 2) continue;

                var p = CoordinateSystemWktReader.Parse(lineToks[1]) as ICoordinateSystem;
                if (p != null) projections.Add(p);
            }

            return projections;
        }

        public static ICoordinateSystem GetCoordinateSystemFromSrid(int srid)
        {
            var projectionLines = SridWkt;

            foreach (var proj in projectionLines)
            {
                var lineToks = proj.Split(';');
                if (lineToks.Length != 2) continue;

                var parsedSrid = -1;
                if (!int.TryParse(lineToks[0], out parsedSrid) || parsedSrid != srid) continue;

                var p = CoordinateSystemWktReader.Parse(lineToks[1]) as ICoordinateSystem;
                return p;
            }

            return null;
        }

        /// <summary>
        /// Reverse lookup of SRID from well known text.
        /// </summary>
        /// <param name="wkt"></param>
        /// <returns></returns>
        public static int FindSrid(string wkt)
        {
            // TODO: Finish!

            var wktCoordinateSystem = CoordinateSystemWktReader.Parse(wkt) as ICoordinateSystem;
            if (wktCoordinateSystem == null) throw new ArgumentException("Cannot parse wkt");

            return wktCoordinateSystem.FindSrid();
        }

        public static int FindSrid(this ICoordinateSystem coordinateSystem)
        {
            var coordinateSystems = ReadAllFromWkt();
            var wktAsProjCS = coordinateSystem as IProjectedCoordinateSystem;
            if (wktAsProjCS != null)
            {
                coordinateSystems = coordinateSystems.Where(cs => cs is IProjectedCoordinateSystem);
                var proj = coordinateSystems
                    .Cast<IProjectedCoordinateSystem>()
                    .Where(cs => cs.GeographicCoordinateSystem.EqualsGeogCS(wktAsProjCS.GeographicCoordinateSystem) &&
                                 cs.Projection.EqualParams(wktAsProjCS.Projection) && cs.LinearUnit.EqualParams(wktAsProjCS.LinearUnit))
                    .FirstOrDefault();

                if (proj != null) return (int)proj.AuthorityCode;
            }
            else if (coordinateSystem as IGeographicCoordinateSystem != null)
            {
                // give pref treatment to WGS84 (in otherwords, if there is any way it could be viewed as wgs84, call it wgs84)
                if (coordinateSystem.IsWgs84()) return (int)GeographicCoordinateSystem.WGS84.AuthorityCode;

                // not wgs84, search
                var wktGeogCS = (IGeographicCoordinateSystem)coordinateSystem;
                var proj = coordinateSystems
                    .Where(cs => cs is IGeographicCoordinateSystem)
                    .Cast<IGeographicCoordinateSystem>()
                    .Where(cs => cs.EqualsGeogCS(wktGeogCS))
                    .FirstOrDefault();

                if (proj != null) return (int)proj.AuthorityCode;
            }

            return 0;
        }

        public static bool EqualsGeogCS(this IGeographicCoordinateSystem geogCS, IGeographicCoordinateSystem otherGeogCS)
        {
            return geogCS.HorizontalDatum.Ellipsoid.SemiMajorAxis == otherGeogCS.HorizontalDatum.Ellipsoid.SemiMajorAxis &&
                   geogCS.HorizontalDatum.Ellipsoid.SemiMinorAxis == otherGeogCS.HorizontalDatum.Ellipsoid.SemiMinorAxis &&
                   geogCS.PrimeMeridian.Longitude == otherGeogCS.PrimeMeridian.Longitude &&
                   Math.Abs(geogCS.AngularUnit.RadiansPerUnit - otherGeogCS.AngularUnit.RadiansPerUnit) < 0.0000000000000001;
        }

        public static bool IsWgs84(string wkt)
        {
            var wktCoordSys = CoordinateSystemWktReader.Parse(wkt) as ICoordinateSystem;
            if (wktCoordSys == null) throw new ArgumentException("Cannot parse coordinate system from wkt");

            return wktCoordSys.IsWgs84();
        }

        public static bool IsWgs84(this ICoordinateSystem coordinateSystem)
        {
            var geogcs = coordinateSystem as IGeographicCoordinateSystem;
            if (geogcs == null) return false;

            var wgs84 = GeographicCoordinateSystem.WGS84;

            return wgs84.AuthorityCode == geogcs.AuthorityCode || geogcs.EqualsGeogCS(wgs84);
        }

        /// <summary>
        /// Gets the UTM projection based on a lat lon
        /// </summary>
        /// <param name="lat"></param>
        /// <param name="lon"></param>
        /// <returns></returns>
        public static ProjectedCoordinateSystem AutoUtm(double lat, double lon)
        {
            // E/W zone == ceil((lon + 180) / 6)
            // N/S == lat >= 0 ? N : S

            return ProjectedCoordinateSystem.WGS84_UTM((int)Math.Ceiling((lon + 180) / 6.0), lat >= 0);
        }

        /// <summary>
        /// Gets the UTM projection based on the current projection of a Geometry.
        /// </summary>
        /// <param name="geom"></param>
        /// <returns></returns>
        public static ProjectedCoordinateSystem AutoUtm(this Geometry geom)
        {
            return AutoUtm(geom.Centroid.Y, geom.Centroid.X);
        }

        /// <summary>
        /// Transforms a geometry to WGS84 UTM (auto identifies the zone).
        /// </summary>
        /// <param name="geom"></param>
        /// <returns></returns>
        /// <exception cref="ArgumentException">The geom does not have an SRID</exception>
        public static Geometry ToUtm(this Geometry geom)
        {
            if (geom.SRID <= 0)
            {
                throw new ArgumentException("Geometry SRID must have a valid value");
            }

            return geom.Transform(geom.AutoUtm());
        }

        /// <summary>
        /// Returns the envelope defined by a world file for an image.  This does not take into account "pixel rotation".
        /// </summary>
        /// <param name="image"></param>
        /// <param name="path"></param>
        /// <param name="orignalBoundary"></param>
        /// <returns></returns>
        /// <exception cref="IOException" />
        /// <exception cref="FormatException">Couldn't parse a double from the line of a world file.</exception>
        public static IEnvelope GetLatLonBoundsFromWorldFile(this Image image, string path, Geometry orignalBoundary)
        {
            var utm = orignalBoundary.AutoUtm();
            var lines = File.ReadAllLines(path);
            var data = new double[6];
            for (int i = 0; i < 6; i++)
            {
                data[i] = double.Parse(lines[i].Trim());
            }

            var envelope = new Envelope(
                data[4], data[4] + image.Width * data[0],
                data[5], data[5] + image.Height * data[3]);

            var polygon = new Polygon(
                            new LinearRing(
                                new ICoordinate[]
                    {
                        new Coordinate(envelope.MinX, envelope.MaxY),
                        new Coordinate(envelope.MaxX, envelope.MaxY),
                        new Coordinate(envelope.MaxX, envelope.MinY),
                        new Coordinate(envelope.MinX, envelope.MinY),
                        new Coordinate(envelope.MinX, envelope.MaxY)
                    }));

            polygon.SRID = utm.FindSrid();
            var wgs84Poly = polygon.Transform(ProjNet.CoordinateSystems.GeographicCoordinateSystem.WGS84);

            var bounds = GeometryFactory.Default.CreatePolygon(new LinearRing(wgs84Poly.Coordinates), null).EnvelopeInternal;
            return bounds;
        }
    }
}